package com.c6906.vdam.das.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.*;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.c6906.vdam.common.util.CoordTransform;
import com.c6906.vdam.common.util.GeomUtils;
import com.c6906.vdam.das.common.EWTypeEnum;
import com.c6906.vdam.das.common.LocationUtil;
import com.c6906.vdam.das.config.redis.RedisUtils;
import com.c6906.vdam.das.dao.TerminalBusinessInfoDAO;
import com.c6906.vdam.das.dao.TerminalDataDAO;
import com.c6906.vdam.das.dao.TerminalHistoryDetailsDAO;
import com.c6906.vdam.das.dao.VehiclesDAO;
import com.c6906.vdam.das.model.dto.FunctionalAreaDTO;
import com.c6906.vdam.das.model.dto.NonTaskTimeDTO;
import com.c6906.vdam.das.model.dto.PreWarnDTO;
import com.c6906.vdam.das.model.entity.*;
import com.c6906.vdam.das.service.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import tech.spiro.addrparser.common.Point;
import tech.spiro.addrparser.common.RegionInfo;
import tech.spiro.addrparser.parser.Location;
import tech.spiro.addrparser.parser.LocationParserEngine;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 预警服务serviceImpl
 */
@Service
public class AlarmServiceImpl implements AlarmService {

    private static final double ALARM_MIN_MILEAGE = 10.0;
    private static final Long ALARM_MIN_MINUTES = 60L;
    private static final Logger logger = LoggerFactory.getLogger(AlarmServiceImpl.class);
    private static final String FENCE_ALARM_HIS_PRE = "AreaAlarmHis:";

    @Resource
    TerminalBusinessInfoDAO terminalBusinessInfoDAO;
    @Resource
    VehiclesDAO vehiclesDAO;
    @Resource
    TerminalDataDAO terminalDataDAO;
    @Resource
    TerminalHistoryDetailsDAO terminalHistoryDetailsDAO;
    @Resource
    LocationUtil locationUtil;
    @Resource
    RedisUtils redisUtils;
    @Resource
    DriverMaintenService driverMaintenService;
    @Resource
    EWVehiclesWarnService ewVehiclesWarnService;
    @Resource
    EWVehiclesWarnDetailService ewVehiclesWarnDetailService;
    @Resource
    AreaCheckService areaCheckService;
    @Resource
    ParkingSpotDetailService parkingSpotDetailService;

    /**
     * 切割非任务时间
     */
    private static List<NonTaskTimeDTO> getNonTaskTime(Date startDate, Date endDate, List<VehiclesTask> taskList) {
        List<NonTaskTimeDTO> nonTaskTimes = new ArrayList<>();
        Date currentStart = startDate;

        for (VehiclesTask task : taskList) {
            // 取申请时间和任务时间的并集并扩展一个小时冗余时间
            Date taskStart = Optional.ofNullable(task.getTripStartTime()).flatMap(d1 -> Optional.ofNullable(task.getRealStartTime()).map(d2 -> d1.before(d2) ? d1 : d2)).orElse(Optional.ofNullable(task.getTripStartTime()).orElse(task.getRealStartTime()));
            taskStart = DateUtil.offsetHour(taskStart, -1);
            Date taskEnd = Optional.ofNullable(task.getTripEndTime()).flatMap(d1 -> Optional.ofNullable(task.getRealEndTime()).map(d2 -> d1.after(d2) ? d1 : d2)).orElse(Optional.ofNullable(task.getTripEndTime()).orElse(task.getRealEndTime()));
            taskEnd = DateUtil.offsetHour(taskEnd, 1);

            // 如果任务开始时间在给定时间段外，则忽略
            if (taskStart.after(endDate) || (taskEnd != null && taskEnd.before(startDate))) {
                continue;
            }

            // 如果任务开始时间早于当前时间段的开始时间，则将任务开始时间设置为当前时间段的开始时间
            if (taskStart.before(currentStart)) {
                taskStart = currentStart;
            }

            // 如果任务有结束时间，处理无任务时间段
            if (taskEnd != null) {
                // 在任务前有空闲时间
                if (taskStart.after(currentStart)) {
                    nonTaskTimes.add(new NonTaskTimeDTO(currentStart, taskStart));
                }
                // 更新当前起始时间为任务结束时间之后
                currentStart = taskEnd.after(currentStart) ? taskEnd : currentStart;
            } else {
                // 如果任务没有结束时间，意味着任务还在进行中，从任务开始时间到时间段结束的部分都被占用
                if (taskStart.before(endDate)) {
                    if (taskStart.after(currentStart)) {
                        nonTaskTimes.add(new NonTaskTimeDTO(currentStart, taskStart));
                    }
                    return nonTaskTimes; // 后续所有时间都被占用
                }
            }
        }

        // 如果还有剩余的时间段没有任务占用，则加入
        if (currentStart.before(endDate)) {
            nonTaskTimes.add(new NonTaskTimeDTO(currentStart, endDate));
        }

        return nonTaskTimes;
    }

    /**
     * 获取分区字段
     */
    private static List<Integer> getPartitionField(Date taskStart, Date taskEnd) {
        Set<Integer> dateSet = new HashSet<>();
        List<DateTime> dateTimes = DateUtil.rangeToList(taskStart, taskEnd, DateField.DAY_OF_YEAR);
        dateTimes.forEach(i -> dateSet.add(Integer.parseInt(DateUtil.format(i, DatePattern.PURE_DATE_PATTERN))));
        return new ArrayList<>(dateSet);
    }

    /**
     * 判断两个位置是否不是市内的各区或同县
     */
    private static boolean isDiffDistrict(Location loc1, Location loc2) {
        if (loc1 != null && loc2 != null) {
            RegionInfo districtInfo1 = loc1.getDistrict();
            RegionInfo districtInfo2 = loc2.getDistrict();
            return districtInfo1.getCode() != districtInfo2.getCode() && !(districtInfo1.getParentCode() == districtInfo2.getParentCode() && districtInfo1.getName().contains("区") && districtInfo2.getName().contains("区"));
        } else {
            return false;
        }
    }

    /**
     * 判断两个位置是否跨市
     */
    private static boolean isDiffCity(Location loc1, Location loc2) {
        if (loc1 != null && loc2 != null) {
            RegionInfo cityInfo1 = loc1.getCity();
            RegionInfo cityInfo2 = loc2.getCity();
            return cityInfo1.getCode() != cityInfo2.getCode();
        } else {
            return false;
        }
    }

    /**
     * 分析车辆在夜间非任务时间段内的违规停车行为（每天07:30执行）
     * <p>
     * 分析逻辑：
     * 1. 确定非任务时间段：当日20:00至次日07:00。
     * 2. 查询该时段内未安排任务的车辆。
     * 3. 提取车辆在此时段内的停车轨迹数据。
     * 4. 判断每一条停车数据是否在合法停车点（使用单位/所属单位/自定义停车点）3公里范围内：
     * - 是：跳过；
     * - 否：标记为违规停车，并记录距离最近的合法点与距离。
     * 5. 记录违规停车段起止时间，若持续跨天则生成预警。
     *
     * @param date 分析日期（格式：yyyy-MM-dd）
     */
    @Override
    public void analyzeIllegalParking(String date) {
        // 创建分析时间段
        Date yesterday8PM = DateUtil.offsetHour(DateUtil.endOfDay(DateUtil.parse(date)), -4);
        Date today7AM = DateUtil.offsetHour(DateUtil.endOfDay(DateUtil.parse(date)), 7);
        NonTaskTimeDTO nonTaskTimeDTO = new NonTaskTimeDTO(yesterday8PM, today7AM);

        List<EWVehiclesWarn> ewVehiclesWarns = new ArrayList<>();
        List<EWVehiclesWarnDetail> ewVehiclesWarnDetails = new ArrayList<>();

        int startRow = 0, offset = 2000;
        while (true) {
            List<TerminalBusinessInfo> terminalBusinessInfoList = terminalBusinessInfoDAO.getTwoDaysNoTaskTerminalBusInfos(startRow, offset, date, DateUtil.formatDate(today7AM));
            if (CollectionUtil.isEmpty(terminalBusinessInfoList)) {
                logger.info("【违规停车】{}车辆已全部分析完毕!", date);
                break;
            }
            logger.info("【违规停车】获取车辆{}辆,参数为startRow={},offset={}", terminalBusinessInfoList.size(), startRow, offset);

            for (TerminalBusinessInfo terminalBusinessInfo : terminalBusinessInfoList) {
                try {
                    BladeUnit useUnit = terminalBusinessInfoDAO.getBladeUnitById(Long.valueOf(terminalBusinessInfo.getUseUnitId()));
                    BladeUnit ownUnit = terminalBusinessInfoDAO.getBladeUnitById(Long.valueOf(terminalBusinessInfo.getOwnUnitId()));

                    // 获取合法停车点并提前转换坐标系
                    List<Point> legalPoints = new ArrayList<>();
                    if (useUnit != null && useUnit.getLongitude() != null && useUnit.getLatitude() != null) {
                        double[] coord = CoordTransform.bd09towgs84(useUnit.getLongitude().doubleValue(), useUnit.getLatitude().doubleValue());
                        legalPoints.add(new Point(coord[0], coord[1]));
                    }
                    if (ownUnit != null && ownUnit.getLongitude() != null && ownUnit.getLatitude() != null) {
                        double[] coord = CoordTransform.bd09towgs84(ownUnit.getLongitude().doubleValue(), ownUnit.getLatitude().doubleValue());
                        legalPoints.add(new Point(coord[0], coord[1]));
                    }
                    List<ParkingSpotDetail> parkingSpots = parkingSpotDetailService.list(
                            new LambdaQueryWrapper<ParkingSpotDetail>()
                                    .in(ParkingSpotDetail::getUnitId, Stream.of(
                                            terminalBusinessInfo.getUseUnitId(),
                                            terminalBusinessInfo.getOwnUnitId()
                                    ).filter(Objects::nonNull).collect(Collectors.toList()))
                                    .eq(ParkingSpotDetail::getIsEnable, 1)
                                    .isNotNull(ParkingSpotDetail::getLongitude)
                                    .isNotNull(ParkingSpotDetail::getLatitude)
                    );
                    if (!parkingSpots.isEmpty()) {
                        legalPoints.addAll(parkingSpots.stream()
                                .map(p -> {
                                    double[] coord = CoordTransform.bd09towgs84(p.getLongitude().doubleValue(), p.getLatitude().doubleValue());
                                    return new Point(coord[0], coord[1]);
                                }).collect(Collectors.toList()));
                    }

                    // 查询车辆停车轨迹
                    List<Integer> partitionField = getPartitionField(nonTaskTimeDTO.getStartDate(), DateUtil.offsetDay(nonTaskTimeDTO.getStartDate(), 1));
                    List<TerminalDMData> parkingDataList = terminalDataDAO.selectParkingDataByCodesAndTime(
                            DateUtil.format(nonTaskTimeDTO.getEndDate(), "yyyy"),
                            DateUtil.format(nonTaskTimeDTO.getEndDate(), "M"),
                            terminalBusinessInfo.getVehiclesCode(),
                            partitionField,
                            DateUtil.formatDateTime(nonTaskTimeDTO.getStartDate()),
                            DateUtil.formatDateTime(nonTaskTimeDTO.getEndDate())
                    );

                    if (CollectionUtil.isEmpty(parkingDataList) || CollectionUtil.isEmpty(legalPoints)) {
                        continue;
                    }

                    // 先判断数据是否合法
                    if (parkingDataList.size() < 2) continue;
                    Date startTime = DateUtil.parse(CollectionUtil.getFirst(parkingDataList).getTerminalTime());
                    Date endTime = DateUtil.parse(CollectionUtil.getLast(parkingDataList).getTerminalTime());
                    if (DateUtil.isSameDay(startTime, endTime)) continue;

                    EWVehiclesIllegalParking illegalParking = null;
                    for (int i = 0; i < parkingDataList.size(); i++) {
                        TerminalDMData data = parkingDataList.get(i);

                        Point nearestPoint = null;
                        double minDistance = Double.MAX_VALUE;
                        boolean isNearParking = false;
                        for (Point point : legalPoints) {
                            double distance = GeomUtils.distance(data.getLongitude().doubleValue(), data.getLatitude().doubleValue(), point.getLon(), point.getLat());
                            if (distance <= 3000.0) {
                                isNearParking = true;
                                break;
                            }
                            if (distance < minDistance) {
                                minDistance = distance;
                                nearestPoint = point;
                            }
                        }

                        if (illegalParking == null) {
                            if (!isNearParking && nearestPoint != null) {
                                illegalParking = new EWVehiclesIllegalParking(data, nearestPoint);
                                illegalParking.setOffsetDistance(BigDecimal.valueOf(minDistance / 1000));
                                if (i == parkingDataList.size() - 1) {
                                    endIllegalParking(illegalParking, data);
                                }
                            }
                        } else {
                            if (illegalParking.getEndTime() == null) {
                                if (isNearParking) {
                                    TerminalDMData actualEndData = parkingDataList.get(i - 1);
                                    endIllegalParking(illegalParking, actualEndData);
                                } else if (i == parkingDataList.size() - 1) {
                                    endIllegalParking(illegalParking, data);
                                }
                            } else {
                                if (DateUtil.isSameDay(illegalParking.getStartTime(), illegalParking.getEndTime())) {
                                    illegalParking = null;
                                }
                            }
                        }
                    }

                    if (illegalParking != null && !DateUtil.isSameDay(illegalParking.getStartTime(), illegalParking.getEndTime())) {
                        EWVehiclesWarn ewVehiclesWarn = new EWVehiclesWarn(illegalParking, terminalBusinessInfo);
                        ewVehiclesWarns.add(ewVehiclesWarn);
                        EWVehiclesWarnDetail ewVehiclesWarnDetail = new EWVehiclesWarnDetail(illegalParking, ewVehiclesWarn, terminalBusinessInfo);
                        ewVehiclesWarnDetails.add(ewVehiclesWarnDetail);
                    }
                } catch (Exception e) {
                    logger.error("【违规停车】车辆{}分析异常: {}", terminalBusinessInfo.getVehPlateNum(), e.getMessage(), e);
                }
            }
            startRow += offset;
        }

        if (!ewVehiclesWarns.isEmpty()) {
            logger.info("【违规停车】插入{}条违规停车记录", ewVehiclesWarns.size());
            ewVehiclesWarnService.saveBatch(ewVehiclesWarns);
            ewVehiclesWarnDetailService.saveBatch(ewVehiclesWarnDetails);
        }
    }

    /**
     * 结束违规停车
     *
     * @param illegalParking
     * @param endData
     */
    private void endIllegalParking(EWVehiclesIllegalParking illegalParking, TerminalDMData endData) {
        if (illegalParking != null && illegalParking.getEndTime() == null) {
            illegalParking.setEndTime(DateUtil.parse(endData.getTerminalTime()));
            illegalParking.setDuration(DateUtil.between(illegalParking.getStartTime(), illegalParking.getEndTime(), DateUnit.MINUTE));
        }
    }

    /**
     * 擅自改变出行线路 05:00
     */
    @Override
    public void analyzeUnauthTask(String date) {
        // 经纬度逆解析引擎
        LocationParserEngine engine = locationUtil.getEngine();
        // 获取日期中结束的已出车
        List<VehiclesTask> vehiclesTasks = vehiclesDAO.selectAlldayEndTaskByDate(date + '%');
        // 插入的预警数据集合
        List<EWVehiclesWarn> ewVehiclesWarns = new ArrayList<>();
        List<EWVehiclesWarnDetail> ewVehiclesWarnDetails = new ArrayList<>();
        // 遍历任务、分析
        for (VehiclesTask vehiclesTask : vehiclesTasks) {
            try {
                /*
                 * 引擎解析结果格式
                 * provInfo:RegionInfo{parentCode=100000, code=320000, name='江苏省',level=PROVINCE},
                 * cityInfo:RegionInfo{parentCode=320000, code=320100, name='南京市', level=CITY},
                 * districtInfo:RegionInfo{parentCode=320100, code=320106, name='鼓楼区', level=DISTRICT}
                 */
                // 获取任务申请的起始和结束点
                double[] startCoord = CoordTransform.bd09togcj02(vehiclesTask.getStartAddrLon().doubleValue(), vehiclesTask.getStartAddrLat().doubleValue());
                double[] targetCoord = CoordTransform.bd09togcj02(vehiclesTask.getTargetAddrLon().doubleValue(), vehiclesTask.getTargetAddrLat().doubleValue());
                Location startLoc = engine.parse(startCoord[0], startCoord[1]);
                Location targetLoc = engine.parse(targetCoord[0], targetCoord[1]);
                // 跨城且省内的任务，到达外省的任务(大部分为用户地址选择有误),暂不判断
                if (isDiffDistrict(startLoc, targetLoc) && targetLoc.getProv().getName().equals("湖南省")) {
                    // 如果计划时间和实际出车时间完全没有交集，不做分析
                    if (vehiclesTask.getTripStartTime().after(vehiclesTask.getRealEndTime()) || vehiclesTask.getTripEndTime().before(vehiclesTask.getRealStartTime())) {
                        continue;
                    }
                    // 轨迹时间范围取任务时间和实际时间的并集
                    Date dateStart = vehiclesTask.getTripStartTime().before(vehiclesTask.getRealStartTime()) ? vehiclesTask.getTripStartTime() : vehiclesTask.getRealStartTime();
                    Date dateEnd = vehiclesTask.getTripEndTime().after(vehiclesTask.getRealEndTime()) ? vehiclesTask.getTripEndTime() : vehiclesTask.getRealEndTime();
                    List<Integer> partitionField = getPartitionField(dateStart, dateEnd);
                    // 车辆行驶轨迹数据
                    List<TerminalDMData> dataList = new ArrayList<>();
                    // 拼接分库分表语句、查询轨迹数据，出车任务一般不超过一个月
                    // 1、同年同月 2、同年相邻两月 3、跨年相邻两月
                    if (DateUtil.year(dateStart) == DateUtil.year(dateEnd)) {
                        String year = DateUtil.format(dateStart, "yyyy");
                        if (DateUtil.isSameMonth(dateStart, dateEnd)) {
                            String month = DateUtil.format(dateStart, "M");
                            // 查询车辆行驶轨迹，取日期，精确到天
                            dataList = terminalDataDAO.selectDataByCodesAndTime(year, month, vehiclesTask.getVehiclesCode(), partitionField, null, null);
                        } else {
                            String monthStart = DateUtil.format(dateStart, "M");
                            String monthEnd = DateUtil.format(dateEnd, "M");
                            List<TerminalDMData> monthStartDataList = terminalDataDAO.selectDataByCodesAndTime(year, monthStart, vehiclesTask.getVehiclesCode(), partitionField, null, null);
                            List<TerminalDMData> monthEndDataList = terminalDataDAO.selectDataByCodesAndTime(year, monthEnd, vehiclesTask.getVehiclesCode(), partitionField, null, null);
                            dataList.addAll(monthStartDataList);
                            dataList.addAll(monthEndDataList);
                        }
                    } else {
                        String yearStart = DateUtil.format(dateStart, "yyyy");
                        String yearEnd = DateUtil.format(dateEnd, "yyyy");
                        String monthStart = "12";
                        String monthEnd = "1";
                        List<TerminalDMData> monthStartDataList = terminalDataDAO.selectDataByCodesAndTime(yearStart, monthStart, vehiclesTask.getVehiclesCode(), partitionField, null, null);
                        dataList.addAll(monthStartDataList);
                        List<TerminalDMData> monthEndDataList = terminalDataDAO.selectDataByCodesAndTime(yearEnd, monthEnd, vehiclesTask.getVehiclesCode(), partitionField, null, null);
                        dataList.addAll(monthEndDataList);
                    }
                    // 分析轨迹数据
                    if (!dataList.isEmpty()) {
                        // 是否达到过目的地（区县即可）
                        boolean flag = false;
                        RegionInfo targetDistrictInfo = targetLoc.getDistrict();
                        // 到过的区域，保证顺序
                        Set<String> locationUniqKey = new HashSet<>();
                        List<Location> dedupedLocations = new ArrayList<>();
                        for (TerminalDMData data : dataList) {
                            double[] curLoc = CoordTransform.wgs84togcj02(data.getLongitude().doubleValue(), data.getLatitude().doubleValue());
                            Location loc = engine.parse(curLoc[0], curLoc[1]);
                            String key = String.valueOf(loc.getDistrict().getCode()); // 或者更多字段
                            if (locationUniqKey.add(key)) {
                                dedupedLocations.add(loc);
                            }
                            RegionInfo curDistrict = loc.getDistrict();
                            if (!flag && targetDistrictInfo.getParentCode() == curDistrict.getParentCode()) {
                                flag = true;
                                /*
                                double miles = data.getTerminalMiles().doubleValue() - dataList.get(0).getTerminalMiles().doubleValue();
                                if (vehiclesTask.getEvalMiles() != null && vehiclesTask.getEvalMiles().doubleValue() > 0.0 && miles - vehiclesTask.getEvalMiles().doubleValue() > 30.0) {
                                    logger.info("ID为{}的任务,实际里程与预估里程相差超过30km", vehiclesTask.getId());
                                    // 记录开始地，取最远的区县作为到达地
                                    Location startLocation = CollectionUtil.getFirst(arriviedRegions);
                                    double[] startPoint = CoordTransform.gcj02towgs84(startLocation.getDistrict().getCenter().getLon(), startLocation.getDistrict().getCenter().getLat());
                                    double maxDistance = 0;
                                    Location farthestLocation = startLocation;
                                    for (Location thisLocation : arriviedRegions) {
                                        Point center = thisLocation.getDistrict().getCenter();
                                        double[] point = CoordTransform.gcj02towgs84(center.getLon(), center.getLat());
                                        double distance = GeomUtils.distance(point[0], point[1], startPoint[0], startPoint[1]);
                                        if (distance > maxDistance) {
                                            maxDistance = distance;
                                            farthestLocation = thisLocation;
                                        }
                                    }
                                    RegionInfo startCityInfo = startLocation.getCity();
                                    RegionInfo startDistrictInfo = startLocation.getDistrict();
                                    vehiclesTask.setRealStartAddr(startCityInfo.getName() + startDistrictInfo.getName());
                                    RegionInfo farthestCityInfo = farthestLocation.getCity();
                                    RegionInfo farthestDistrictInfo = farthestLocation.getDistrict();
                                    // 创建预警并插入
                                    TerminalBusinessInfo terminalBusinessInfo = redisUtils.terminalInfoIsExistFormRedis("SN" + data.getTerminalSn());
                                    vehiclesTask.setRealEndAddr(farthestCityInfo.getName() + farthestDistrictInfo.getName());
                                    EWVehiclesUnauthTask ewVehiclesUnauthTask = new EWVehiclesUnauthTask(terminalBusinessInfo, vehiclesTask, 2);
                                    EWVehiclesWarn ewVehiclesWarn = new EWVehiclesWarn(ewVehiclesUnauthTask, terminalBusinessInfo);
                                    ewVehiclesWarn.setEwRemark(ewVehiclesWarn.getEwRemark().replace("目的地不符", "里程偏差超过30公里"));
                                    EWVehiclesWarnDetail ewVehiclesWarnDetail = new EWVehiclesWarnDetail(ewVehiclesUnauthTask, ewVehiclesWarn, terminalBusinessInfo);
                                    ewVehiclesWarns.add(ewVehiclesWarn);
                                    ewVehiclesWarnDetails.add(ewVehiclesWarnDetail);
                                }
                                */
                            }
                        }
                        // 没到过目的地则生成预警并插入
                        if (!flag && dedupedLocations.size() > 1) {
                            boolean outDistrict = false;
                            Location startLocation = CollectionUtil.getFirst(dedupedLocations);
                            // 最远的区县作为到达地，并判断是否有跨城，未跨城不生成预警
                            Location farthestLocation = startLocation;
                            double[] startPoint = CoordTransform.gcj02towgs84(startLocation.getDistrict().getCenter().getLon(), startLocation.getDistrict().getCenter().getLat());
                            double maxDistance = 0;
                            for (Location thisLocation : dedupedLocations) {
                                Point center = thisLocation.getDistrict().getCenter();
                                double[] point = CoordTransform.gcj02towgs84(center.getLon(), center.getLat());
                                double distance = GeomUtils.distance(point[0], point[1], startPoint[0], startPoint[1]);
                                if (distance > maxDistance) {
                                    maxDistance = distance;
                                    farthestLocation = thisLocation;
                                }
                                if (!outDistrict && isDiffDistrict(startLocation, thisLocation)) {
                                    outDistrict = true;
                                }
                            }
                            if (outDistrict) {
                                logger.info("ID为{}的任务,实际到达地与申请目的地不符", vehiclesTask.getId());
                                // 拼接实际到达地
                                RegionInfo startCityInfo = startLocation.getCity();
                                RegionInfo startDistrictInfo = startLocation.getDistrict();
                                vehiclesTask.setRealStartAddr(startCityInfo.getName() + startDistrictInfo.getName());
                                RegionInfo farthestCityInfo = farthestLocation.getCity();
                                RegionInfo farthestDistrictInfo = farthestLocation.getDistrict();
                                vehiclesTask.setRealEndAddr(farthestCityInfo.getName() + farthestDistrictInfo.getName());
                                TerminalBusinessInfo terminalBusinessInfo = redisUtils.terminalInfoIsExistFormRedis("SN" + dataList.get(0).getTerminalSn());
                                EWVehiclesUnauthTask ewVehiclesUnauthTask = new EWVehiclesUnauthTask(terminalBusinessInfo, vehiclesTask);
                                EWVehiclesWarn ewVehiclesWarn = new EWVehiclesWarn(ewVehiclesUnauthTask, terminalBusinessInfo);
                                EWVehiclesWarnDetail ewVehiclesWarnDetail = new EWVehiclesWarnDetail(ewVehiclesUnauthTask, ewVehiclesWarn, terminalBusinessInfo);
                                ewVehiclesWarns.add(ewVehiclesWarn);
                                ewVehiclesWarnDetails.add(ewVehiclesWarnDetail);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                logger.error("【擅自改变出行线路】车牌号为{}的车辆出现异常", vehiclesTask.getVehPlateNum(), e);
            }
        }
        ewVehiclesWarnService.saveBatch(ewVehiclesWarns);
        ewVehiclesWarnDetailService.saveBatch(ewVehiclesWarnDetails);
    }

    /**
     * 公车私用行为分析（每天凌晨03:00执行）
     *
     * 根据节假日设置判断当日是否为工作日/非工作日，分别创建工作时间段和非工作时间段。
     * 对于没有任务安排的车辆，分析其在这些时间段内是否存在行驶行为，若行驶距离超过阈值，
     * 且行驶时间超过设定时间，且发生地存在明显行政区划迁移，则判定为“公车私用”行为。
     *
     * 预警条件：
     * - 行驶里程 > ALARM_MIN_MILEAGE（默认10km）
     * - ACC持续开启时长 > ALARM_MIN_MINUTES（默认60分钟）
     * - 存在行政区划迁移（跨区）
     *
     * @param date 分析的日期（格式：yyyy-MM-dd）
     */
    @Override
    public void analyzeNoTask(String date) {
        LocationParserEngine engine = locationUtil.getEngine();
        Date applyDate = DateUtil.parseDate(date);
        // 获取节假日设置
        BladeHolidaySetting bladeHolidaySetting = vehiclesDAO.getHolidaySetting(DateUtil.format(applyDate, "yyyy-MM-dd"));
        List<NonTaskTimeDTO> noWorkTimeDTOList = new ArrayList<>();
        List<NonTaskTimeDTO> workTimeDTOList = new ArrayList<>();
        String dayType;
        if (ObjectUtils.isEmpty(bladeHolidaySetting) || StrUtil.isBlank(bladeHolidaySetting.getHolidayName()) || bladeHolidaySetting.getHolidayName().equals("补班")) {
            // 工作日取下班时间分析（工作日和补班）
            dayType = StrUtil.isBlank(bladeHolidaySetting.getHolidayName()) ? "工作日" : bladeHolidaySetting.getHolidayName();
            logger.info("【无任务出车】{}为工作日,上班时间和下班时间分开分析", DateUtil.formatDate(applyDate));
            // 创建上班-昨天早上7点的 Date 对象
            Date yesterday7AM = DateUtil.offsetHour(DateUtil.beginOfDay(applyDate), 7);
            // 创建下班-昨天晚上8点的 Date 对象
            Date yesterday8PM = DateUtil.offsetHour(DateUtil.beginOfDay(applyDate), 20);
            noWorkTimeDTOList.add(new NonTaskTimeDTO(DateUtil.beginOfDay(applyDate), yesterday7AM));
            noWorkTimeDTOList.add(new NonTaskTimeDTO(yesterday8PM, DateUtil.endOfDay(applyDate)));
            workTimeDTOList.add(new NonTaskTimeDTO(yesterday7AM, yesterday8PM));
        } else {
            // 非工作日取全天时间分析（周末、节假日、调休）
            dayType = bladeHolidaySetting.getHolidayName().equals("休") ? "周末" : bladeHolidaySetting.getHolidayName();
            logger.info("【无任务出车】{}为非工作日,取全天时间分析", DateUtil.formatDate(applyDate));
            noWorkTimeDTOList.add(new NonTaskTimeDTO(DateUtil.beginOfDay(applyDate), DateUtil.endOfDay(applyDate)));
        }

        // 获取昨天需要分析的非工作\工作时间段，分页遍历车辆表中有安装终端的车辆
        int startRow = 0, offset = 100;
        while (true) {
            List<TerminalBusinessInfo> terminalBusinessInfoList = terminalBusinessInfoDAO.getTerminalBusinessInfoList(startRow, offset);
            if (!CollectionUtil.isEmpty(terminalBusinessInfoList)) {
                logger.info("【无任务出车】获取车辆{}辆,参数为startRow={},offset={}", terminalBusinessInfoList.size(), startRow, offset);
                List<EWVehiclesWarn> ewVehiclesWarnList = new ArrayList<>();
                List<EWVehiclesWarnDetail> ewVehiclesWarnDetailList = new ArrayList<>();
                for (TerminalBusinessInfo terminalBusinessInfo : terminalBusinessInfoList) {
                    // 当天有申请、任务的车直接跳过分析
                    List<VehiclesTask> vehiclesTaskList = vehiclesDAO.selectTaskByCodeAndDate(terminalBusinessInfo.getVehiclesCode(), date);
                    if (!vehiclesTaskList.isEmpty()) continue;

                    boolean flag = false;
                    String vehiclesCode = terminalBusinessInfo.getVehiclesCode();
                    // 工作时间段无任务出车,达到阈值产生预警
                    for (NonTaskTimeDTO workTime : workTimeDTOList) {
                        // 获取当前工作时间的保养记录,有保养记录则跳过
                        List<DriverMainten> driverMaintenList = driverMaintenService.lambdaQuery().eq(DriverMainten::getVehiclesCode, vehiclesCode).le(DriverMainten::getStartTime, workTime.getEndDate()).ge(DriverMainten::getEndTime, workTime.getStartDate()).list();
                        if (!CollectionUtil.isEmpty(driverMaintenList)) continue;
                        // 获取当前工作时间段的任务
                        List<VehiclesTask> vehiclesTasks = vehiclesDAO.selectTaskByCodeAndTime(vehiclesCode, workTime.getStartDate(), workTime.getEndDate());
                        // 将工作时间的任务时间段切除
                        List<NonTaskTimeDTO> workNoTaskTimeList = getNonTaskTime(workTime.getStartDate(), workTime.getEndDate(), vehiclesTasks);
                        for (NonTaskTimeDTO workNoTask : workNoTaskTimeList) {
                            String partitionField = "P" + DateUtil.format(workNoTask.getEndDate(), "yyyyMMdd");
                            String year = DateUtil.format(workNoTask.getEndDate(), "yyyy");
                            String month = DateUtil.format(workNoTask.getEndDate(), "M");
                            List<TerminalDMData> terminalDatas = terminalDataDAO.selectDrivingDataByCodesAndTime(year, month, vehiclesCode, partitionField, DateUtil.formatDateTime(workNoTask.getStartDate()), DateUtil.formatDateTime(workNoTask.getEndDate()));
                            if (!terminalDatas.isEmpty()) {
                                Long maxDuration = calculateAccOnDurations(terminalDatas);
                                Double miles = calculateAccOnMiles(terminalDatas);
                                if (miles > ALARM_MIN_MILEAGE || maxDuration > ALARM_MIN_MINUTES) {
                                    // 因为引用的Location没有实现hashCode和equals方法，无法直接使用Set去重
                                    Set<String> locationUniqKey = new HashSet<>();
                                    List<Location> dedupedLocations = new ArrayList<>();
                                    terminalDatas.forEach(data -> {
                                        double[] curLoc = CoordTransform.wgs84togcj02(data.getLongitude().doubleValue(), data.getLatitude().doubleValue());
                                        Location loc = engine.parse(curLoc[0], curLoc[1]);
                                        String key = String.valueOf(loc.getDistrict().getCode()); // 或者更多字段
                                        if (locationUniqKey.add(key)) {
                                            dedupedLocations.add(loc);
                                        }
                                    });
                                    Location startLocation = CollectionUtil.getFirst(dedupedLocations);
                                    Location farthestLocation = startLocation;
                                    double[] startPoint = CoordTransform.gcj02towgs84(startLocation.getDistrict().getCenter().getLon(), startLocation.getDistrict().getCenter().getLat());
                                    double maxDistance = 0;
                                    for (Location thisLocation : dedupedLocations) {
                                        Point center = thisLocation.getDistrict().getCenter();
                                        double[] point = CoordTransform.gcj02towgs84(center.getLon(), center.getLat());
                                        double distance = GeomUtils.distance(point[0], point[1], startPoint[0], startPoint[1]);
                                        if (distance > maxDistance) {
                                            maxDistance = distance;
                                            farthestLocation = thisLocation;
                                        }
                                    }
                                    EWVehiclesNoTask ewVehiclesNoTask = new EWVehiclesNoTask(CollectionUtil.getFirst(terminalDatas), CollectionUtil.getLast(terminalDatas), miles, dayType);
                                    ewVehiclesNoTask.setDuration(maxDuration);
                                    ewVehiclesNoTask.setStartAddr(startLocation.getCity().getName() + startLocation.getDistrict().getName());
                                    ewVehiclesNoTask.setEndAddr(farthestLocation.getCity().getName() + farthestLocation.getDistrict().getName());
                                    EWVehiclesWarn ewVehiclesWarn = new EWVehiclesWarn(ewVehiclesNoTask, terminalBusinessInfo);
                                    EWVehiclesWarnDetail ewVehiclesWarnDetail = new EWVehiclesWarnDetail(ewVehiclesNoTask, ewVehiclesWarn, terminalBusinessInfo);
                                    ewVehiclesWarnList.add(ewVehiclesWarn);
                                    ewVehiclesWarnDetailList.add(ewVehiclesWarnDetail);
                                    flag = true;
                                    break;
                                }
                            }
                        }
                        if (flag) break;
                    }

                    // 一天一辆车只生成一条预警
                    if (flag) continue;

                    // 非工作时间段无任务出车，达到阈值产生预警
                    for (NonTaskTimeDTO nonWorkTime : noWorkTimeDTOList) {
                        List<VehiclesTask> vehiclesTasks = vehiclesDAO.selectTaskByCodeAndTime(vehiclesCode, nonWorkTime.getStartDate(), nonWorkTime.getEndDate());
                        // 将非工作时间的任务时间段切除
                        List<NonTaskTimeDTO> noWorkNoTaskTimeList = getNonTaskTime(nonWorkTime.getStartDate(), nonWorkTime.getEndDate(), vehiclesTasks);
                        for (NonTaskTimeDTO noWorkNoTask : noWorkNoTaskTimeList) {
                            // 无任务出车，不会跨天，可以使用分区字段提升查询速度
                            String partitionField = "P" + DateUtil.format(nonWorkTime.getEndDate(), "yyyyMMdd");
                            String year = DateUtil.format(noWorkNoTask.getEndDate(), "yyyy");
                            String month = DateUtil.format(noWorkNoTask.getEndDate(), "M");
                            List<TerminalDMData> terminalDatas = terminalDataDAO.selectDrivingDataByCodesAndTime(year, month, vehiclesCode, partitionField, DateUtil.formatDateTime(noWorkNoTask.getStartDate()), DateUtil.formatDateTime(noWorkNoTask.getEndDate()));
                            if (terminalDatas.size() > 1) {
                                Double miles = calculateAccOnMiles(terminalDatas);
                                if (miles > 3.0) {
                                    EWVehiclesNoTask ewVehiclesNoTask = new EWVehiclesNoTask(CollectionUtil.getFirst(terminalDatas), CollectionUtil.getLast(terminalDatas), miles, dayType);
                                    double[] startLoc = CoordTransform.wgs84togcj02(ewVehiclesNoTask.getStartLon().doubleValue(), ewVehiclesNoTask.getStartLat().doubleValue());
                                    double[] endLoc = CoordTransform.wgs84togcj02(ewVehiclesNoTask.getEndLon().doubleValue(), ewVehiclesNoTask.getEndLat().doubleValue());
                                    Location startLocInfo = engine.parse(startLoc[0], startLoc[1]);
                                    Location endLocInfo = engine.parse(endLoc[0], endLoc[1]);
                                    ewVehiclesNoTask.setStartAddr(startLocInfo.getCity().getName() + startLocInfo.getDistrict().getName());
                                    ewVehiclesNoTask.setEndAddr(endLocInfo.getCity().getName() + endLocInfo.getDistrict().getName());
                                    EWVehiclesWarn ewVehiclesWarn = new EWVehiclesWarn(ewVehiclesNoTask, terminalBusinessInfo);
                                    EWVehiclesWarnDetail ewVehiclesWarnDetail = new EWVehiclesWarnDetail(ewVehiclesNoTask, ewVehiclesWarn, terminalBusinessInfo);
                                    ewVehiclesWarnList.add(ewVehiclesWarn);
                                    ewVehiclesWarnDetailList.add(ewVehiclesWarnDetail);
                                    flag = true;
                                    break;
                                }
                            }
                        }
                        if (flag) break;
                    }
                }
                // 批量插入无任务出车记录
                if (!ewVehiclesWarnList.isEmpty()) {
                    logger.info("插入{}条无任务出车记录", ewVehiclesWarnList.size());
                    ewVehiclesWarnService.saveBatch(ewVehiclesWarnList);
                    ewVehiclesWarnDetailService.saveBatch(ewVehiclesWarnDetailList);
                }
                startRow += offset;
            } else {
                logger.info("【无任务出车】车辆已全部分析完毕,任务即将结束!");
                break;
            }
        }
    }


    /**
     * 计算连续ACC为1的最长持续时间（单位：分钟）。
     * 遍历终端数据列表，识别ACC为1的连续时间段，记录每段的时长，
     * 并在每段结束时计算该段的平均速度（里程差 / 时间差），
     * 只有平均速度 >= 5 km/h 的时间段才被记录（安装问题导致部分终端ACC一直为1）。
     * 最终返回满足条件的最长时长。
     *
     * 限制条件：
     * - 时间段平均速度需 >= 5 km/h 才纳入统计
     *
     * @param tempDataList 终端数据列表，需按时间顺序排列
     * @return 最长的符合平均速度要求的ACC=1连续时间段，单位为分钟；如果列表为空或无有效段，则返回0
     */
    private Long calculateAccOnDurations(List<TerminalDMData> tempDataList) {
        if (CollectionUtil.isEmpty(tempDataList)) {
            return 0L;
        }

        List<Long> durations = new ArrayList<>();
        Date startTime = null;
        TerminalDMData prevData = null;

        for (TerminalDMData data : tempDataList) {
            if (data.getTerminalAccStatus() == 1) {
                if (startTime == null) {
                    startTime = DateUtil.parse(data.getTerminalTime());
                }
                prevData = data;
            } else {
                if (startTime != null) {
                    Date endTime = DateUtil.parse(prevData.getTerminalTime());
                    long duration = DateUtil.between(startTime, endTime, DateUnit.MINUTE);
                    // 计算平均速度
                    final Date finalStartTime = startTime;
                    double startMiles = tempDataList.stream()
                            .filter(d -> DateUtil.parse(d.getTerminalTime()).equals(finalStartTime))
                            .findFirst()
                            .map(d -> d.getTerminalMiles().doubleValue())
                            .orElse(0.0);
                    double endMiles = prevData.getTerminalMiles().doubleValue();
                    double milesDiff = endMiles - startMiles;
                    double avgSpeed = duration > 0 ? (milesDiff / (duration / 60.0)) : 0.0;
                    if (avgSpeed >= 5.0) {
                        durations.add(duration);
                    }
                    startTime = null;
                    prevData = null;
                }
            }
        }

        // 最后一段处理
        if (startTime != null) {
            Date endTime = DateUtil.parse(prevData.getTerminalTime());
            long duration = DateUtil.between(startTime, endTime, DateUnit.MINUTE);
            final Date finalStartTime = startTime;
            double startMiles = tempDataList.stream()
                    .filter(d -> DateUtil.parse(d.getTerminalTime()).equals(finalStartTime))
                    .findFirst()
                    .map(d -> d.getTerminalMiles().doubleValue())
                    .orElse(0.0);
            double endMiles = prevData.getTerminalMiles().doubleValue();
            double milesDiff = endMiles - startMiles;
            double avgSpeed = duration > 0 ? (milesDiff / (duration / 60.0)) : 0.0;
            if (avgSpeed >= 5.0) {
                durations.add(duration);
            }
        }

        return durations.isEmpty() ? 0L : Collections.max(durations);
    }

    /**
     * 计算ACC=1状态下的总行驶里程（单位：公里）。
     * 遍历终端数据列表，累计ACC=1状态下相邻终端的里程差值，
     * 并进行累加作为总里程。
     *
     * @param tempDataList 终端数据列表，需按时间顺序排列
     * @return ACC=1状态下的总里程差值（单位：公里）；如果列表为空或无有效段，则返回0.0
     */
    private Double calculateAccOnMiles(List<TerminalDMData> tempDataList) {
        if (CollectionUtil.isEmpty(tempDataList)) {
            return 0.0;
        }

        double totalMiles = 0.0;
        TerminalDMData prevData = null;

        for (TerminalDMData data : tempDataList) {
            if (data.getTerminalAccStatus() == 1) {
                if (prevData != null) {
                    double startMiles = prevData.getTerminalMiles().doubleValue();
                    double endMiles = data.getTerminalMiles().doubleValue();
                    if (endMiles > startMiles) {
                        totalMiles += (endMiles - startMiles);
                    }
                }
                prevData = data;
            } else {
                prevData = null;
            }
        }

        return totalMiles;
    }

    /**
     * 违规出入特定场所 历史数据分析
     */
    @Override
    public void analyzeIllegalPlace(String date) {
        Date applyDate = DateUtil.parse(date);
        int startRow = 0, offset = 1000;
        List<Integer> partitionField = new ArrayList<>();
        partitionField.add(Integer.parseInt(DateUtil.format(applyDate, "yyyyMMdd")));
        String year = DateUtil.format(applyDate, "yyyy");
        String month = DateUtil.format(applyDate, "M");
        while (true) {
            // 获取当天没有任务的车辆
            List<TerminalBusinessInfo> terminalBusinessInfoList = terminalBusinessInfoDAO.getNoTaskTerminalBusInfos(startRow, offset, date);
//        List<TerminalBusinessInfo> terminalBusinessInfoList = new ArrayList<>();
//        TerminalBusinessInfo tbi = redisUtils.terminalInfoIsExistFormRedis("SN014164004304");
//        terminalBusinessInfoList.add(tbi);
            if (!CollectionUtil.isEmpty(terminalBusinessInfoList)) {
                for (TerminalBusinessInfo terminalBusinessInfo : terminalBusinessInfoList) {
                    // 因为查询都是全天的，所以把需要查询日期的分区字段带上即可
//                    List<TerminalDMData> terminalDMDataList = terminalDataDAO.selectDataByCodesAndTime(year, month, terminalBusinessInfo.getVehiclesCode(), partitionField, DateUtil.formatDateTime(DateUtil.beginOfDay(applyDate)), DateUtil.formatDateTime(DateUtil.endOfDay(applyDate)));
                    List<TerminalDMData> terminalDMDataList = terminalDataDAO.selectDataByCodesAndTime(year, month, terminalBusinessInfo.getVehiclesCode(), partitionField, null, null);
                    for (TerminalDMData terminalDMData : terminalDMDataList) {
                        List<FunctionalAreaDTO> fenceList = redisUtils.getFunctionalAreas();
                        List<Long> whiteList = areaCheckService.getAreaWhiteListByVehiclesCode(terminalBusinessInfo.getVehiclesCode());
                        // 存在禁区配置则处理,不存在则不做禁区判断
                        if (null != fenceList && !fenceList.isEmpty()) {
                            // 获取当前车辆是否有预警信息在redis中
                            String alarm = (String) redisUtils.get(FENCE_ALARM_HIS_PRE + terminalDMData.getTerminalSn());
                            EWVehicleDetail ewVehicleDetail = JSON.parseObject(alarm, EWVehicleDetail.class);
                            // 没有预警,判断是否需要生成预警
                            if (ObjectUtils.isEmpty(ewVehicleDetail)) {
                                for (FunctionalAreaDTO fence : fenceList) {
                                    // 白名单车辆不判断禁区 && 三个国家级景区当市的车不判断
                                    if (!CollectionUtil.contains(whiteList, fence.getId()) && !(fence.getAreaName().equals("张家界武陵源") && terminalBusinessInfo.getRegionCode().startsWith("4308")) && !(fence.getAreaName().equals("凤凰古城") && terminalBusinessInfo.getRegionCode().startsWith("4331")) && !(fence.getAreaName().equals("南岳衡山") && terminalBusinessInfo.getRegionCode().startsWith("4304"))) {
                                        if (areaCheckService.isPoiWithinNoParkingArea(terminalDMData.getLongitude().toString(), terminalDMData.getLatitude().toString(), fence.getArea(), fence.getMinArea(), fence.getMaxArea())) { // 进入禁区,创建未生效预警
                                            EWVehicleDetail newEWVehicleDetail = new EWVehicleDetail(fence, terminalBusinessInfo);
                                            newEWVehicleDetail.setFirstInAreaTime(DateUtil.parseDateTime(terminalDMData.getTerminalTime()));
                                            // acc为0或速度为0
                                            if (terminalDMData.getTerminalAccStatus() == 0 || terminalDMData.getSpeed().doubleValue() == 0.0) {// todo
                                                newEWVehicleDetail.setFirstParkAreaTime(DateUtil.parseDateTime(terminalDMData.getTerminalTime()));
                                            }
                                            newEWVehicleDetail.setStartTime(DateUtil.parseDateTime(terminalDMData.getTerminalTime()));
                                            newEWVehicleDetail.setStartMileage(terminalDMData.getTerminalMiles().doubleValue());
                                            newEWVehicleDetail.setStartLocLat(terminalDMData.getLatitude().doubleValue());
                                            newEWVehicleDetail.setStartLocLng(terminalDMData.getLongitude().doubleValue());
                                            redisUtils.set(FENCE_ALARM_HIS_PRE + terminalDMData.getTerminalSn(), JSON.toJSONString(newEWVehicleDetail));
                                            break;
                                        }
                                    }
                                }
                            } else if (ewVehicleDetail.getStatusFlag() == 0) { // 有未生效预警,判断是否需要生效或清理
                                FunctionalAreaDTO functionalArea = fenceList.stream().filter(fence -> fence.getId().equals(ewVehicleDetail.getFenceId())).findFirst().orElse(null);
                                if (null != functionalArea) {
                                    // 在禁区内,判断是否满足预警生效条件
                                    if (areaCheckService.isPoiWithinNoParkingArea(terminalDMData.getLongitude().toString(), terminalDMData.getLatitude().toString(), functionalArea.getArea(), functionalArea.getMinArea(), functionalArea.getMaxArea())) {
                                        if (ewVehicleDetail.getFirstInAreaTime() != null) {
                                            long duration = DateUtil.between(ewVehicleDetail.getFirstInAreaTime(), DateUtil.parseDateTime(terminalDMData.getTerminalTime()), DateUnit.MINUTE);
//                                            if (duration > functionalArea.getEnterStayTime() || duration > functionalArea.getEnterStopTime()) {
                                            if (duration > 2 || duration > functionalArea.getEnterStopTime()) {
                                                ewVehicleDetail.setStatusFlag(1);
                                                ewVehicleDetail.setCreateTime(DateUtil.date());
                                            } else {
                                                ewVehicleDetail.setFirstInAreaTime(DateUtil.parseDateTime(terminalDMData.getTerminalTime()));
                                            }
                                            redisUtils.set(FENCE_ALARM_HIS_PRE + terminalDMData.getTerminalSn(), JSON.toJSONString(ewVehicleDetail));
                                        } else { // 预警未生效,车辆已出禁区,则清理预警信息
                                            redisUtils.delete(FENCE_ALARM_HIS_PRE + terminalDMData.getTerminalSn());
                                        }
                                    }
                                }
                            } else if (ewVehicleDetail.getStatusFlag() == 1) { // 有已生效未结束预警,判断是否需要结束
                                FunctionalAreaDTO functionalArea = fenceList.stream().filter(fence -> fence.getId().equals(ewVehicleDetail.getFenceId())).findFirst().orElse(null);
                                if (null != functionalArea) {
                                    // 出围栏则判断是否满足时间条件
                                    if (!areaCheckService.isPoiWithinNoParkingArea(terminalDMData.getLongitude().toString(), terminalDMData.getLatitude().toString(), functionalArea.getArea(), functionalArea.getMinArea(), functionalArea.getMaxArea())) {
                                        if (ewVehicleDetail.getFirstOutAreaTime() == null) {
                                            Date terminalTime = DateUtil.parseDateTime(terminalDMData.getTerminalTime());
                                            ewVehicleDetail.setFirstOutAreaTime(terminalTime);
                                            if (ewVehicleDetail.getEndTime() == null) {
                                                ewVehicleDetail.setEndLocLng(terminalDMData.getLongitude().doubleValue());
                                                ewVehicleDetail.setEndLocLat(terminalDMData.getLatitude().doubleValue());
                                                ewVehicleDetail.setEndTime(terminalTime);
                                                ewVehicleDetail.setEndMileage(terminalDMData.getTerminalMiles().doubleValue());
                                                ewVehicleDetail.setMileage(NumberUtil.sub(ewVehicleDetail.getEndMileage(), ewVehicleDetail.getStartMileage()));
                                                ewVehicleDetail.setDuration(DateUtil.between(ewVehicleDetail.getEndTime(), ewVehicleDetail.getStartTime(), DateUnit.MINUTE));
                                            }
                                            redisUtils.set(FENCE_ALARM_HIS_PRE + terminalDMData.getTerminalSn(), JSON.toJSONString(ewVehicleDetail));
                                        } else {
                                            long duration = DateUtil.between(ewVehicleDetail.getFirstOutAreaTime(), DateUtil.parseDateTime(terminalDMData.getTerminalTime()), DateUnit.MINUTE);
                                            double mileage = NumberUtil.sub(ewVehicleDetail.getEndMileage(), ewVehicleDetail.getStartMileage());
                                            if (duration >= functionalArea.getLeaveStayTime() || mileage >= functionalArea.getLeaveStopTime()) {
                                                ewVehicleDetail.setUpdateTime(DateUtil.date());
                                                // 数据库插入预警记录
                                                EWVehiclesWarn ewVehiclesWarn = new EWVehiclesWarn(ewVehicleDetail, terminalBusinessInfo);
                                                VehiclesTask vehiclesTask = vehiclesDAO.selectRuningTaskByPlate(terminalDMData.getVehiclesCode());
                                                if (null != vehiclesTask) {
                                                    ewVehiclesWarn.setObjectCode(vehiclesTask.getUseCode());
                                                }
                                                ewVehicleDetail.setEwRemark(ewVehiclesWarn.getEwRemark());
                                                EWVehiclesWarnDetail ewVehiclesWarnDetail = new EWVehiclesWarnDetail(ewVehicleDetail, ewVehiclesWarn, terminalBusinessInfo);
                                                ewVehiclesWarnService.save(ewVehiclesWarn);
                                                ewVehiclesWarnDetailService.save(ewVehiclesWarnDetail);
                                                // 更新完数据库清除redis中告警信息
                                                redisUtils.delete(FENCE_ALARM_HIS_PRE + terminalDMData.getTerminalSn());
                                            }
                                        }
                                    } else { // 还在围栏中,清空出围栏时间
                                        ewVehicleDetail.setFirstOutAreaTime(null);
                                        ewVehicleDetail.setFirstParkOutAreaTime(null);
                                        ewVehicleDetail.setEndLocLng(null);
                                        ewVehicleDetail.setEndLocLat(null);
                                        ewVehicleDetail.setEndMileage(null);
                                        ewVehicleDetail.setEndTime(null);
                                        ewVehicleDetail.setMileage(null);
                                        ewVehicleDetail.setDuration(null);
                                        redisUtils.set(FENCE_ALARM_HIS_PRE + terminalDMData.getTerminalSn(), JSON.toJSONString(ewVehicleDetail));
                                    }
                                }
                            } else {
                                ewVehicleDetail.setStatusFlag(0);
                                redisUtils.set(FENCE_ALARM_HIS_PRE + terminalDMData.getTerminalSn(), JSON.toJSONString(ewVehicleDetail));
                            }
                        }
                    }
                }
            } else {
                break;
            }
            startRow += offset;
        }
        // 删除分析用到的所有key
        redisUtils.delByPrefix(FENCE_ALARM_HIS_PRE);
    }

    /**
     * 终端离线异常，每2分钟执行
     */
    @Override
    public void analyzeOfflineVehicles() {
        // 更新已上线离线记录
        terminalHistoryDetailsDAO.updateErrorDetailsByLastGps();
        logger.info("更新已上线离线异常记录成功");
        // 插入新增离线记录
        terminalHistoryDetailsDAO.insertErrorDetailsByLastGps();
        logger.info("新增离线异常记录成功");
    }

    @Override
    public void endErrors() {
        // 更新不需要的离线记录
        terminalHistoryDetailsDAO.updateRedundantErrorDetails();
        logger.info("更新多余的离线异常记录成功");
    }

    /**
     * 判断车辆是否在当日存在任务
     */
    @Override
    @Cacheable(value = "vehWithTaskCache", key = "#vehiclesCode + '_' + #date")
    public boolean ifVehWithTask(String vehiclesCode, String date) {
        List<VehiclesTask> vehiclesTaskList = vehiclesDAO.selectTaskByCodeAndDate(vehiclesCode, date);
        return !vehiclesTaskList.isEmpty();
    }

    /**
     * 按月分析预警
     */
    @Override
    public void analyzeWarnMonth(Date start, Date end) {
        List<EWVehiclesWarn> ewVehiclesWarnList = new ArrayList<>();
        // 公车私用-单车当月5次及以上
        List<PreWarnDTO> preWarn1 = ewVehiclesWarnService.getWarnVehMonth(EWTypeEnum.NO_TASK.getId(), start, end, 5);
        for (PreWarnDTO preWarnDTO : preWarn1) {
            TerminalBusinessInfo terminalBusinessInfo = terminalBusinessInfoDAO.getTerminalBusinessByVehiclesCode(preWarnDTO.getVehiclesCode());
            EWVehiclesWarn ewVehiclesWarn = new EWVehiclesWarn(start, preWarnDTO.getEwIds(), terminalBusinessInfo, preWarnDTO.getCount(), EWTypeEnum.NO_TASK, "5");
            ewVehiclesWarnList.add(ewVehiclesWarn);
        }
        // todo 公车私用-本级无任务出车蓝色预警超过任务出车比例20%黄色预警，超过30%橙色预警
        // todo 1.当月同一车辆出入同一生活小区5次蓝色预警，10次及以上黄色预警。
        // todo 2.当月同一单位不同车辆出入同一生活小区10次及以上黄色预警。
        // 擅自改变出行路线-单车当月3次及以上
        List<PreWarnDTO> preWarn2 = ewVehiclesWarnService.getWarnVehMonth(EWTypeEnum.UNAUTH_TASK.getId(), start, end, 3);
        for (PreWarnDTO preWarnDTO : preWarn2) {
            TerminalBusinessInfo terminalBusinessInfo = terminalBusinessInfoDAO.getTerminalBusinessByVehiclesCode(preWarnDTO.getVehiclesCode());
            EWVehiclesWarn ewVehiclesWarn = new EWVehiclesWarn(start, preWarnDTO.getEwIds(), terminalBusinessInfo, preWarnDTO.getCount(), EWTypeEnum.UNAUTH_TASK, "3");
            ewVehiclesWarnList.add(ewVehiclesWarn);
        }
        ewVehiclesWarnService.saveBatch(ewVehiclesWarnList);
        logger.info("【公车预警汇总】插入{}条记录......", ewVehiclesWarnList.size());
    }
}
